/* * Copyright (c) 2003-onwards Shaven Puppy Ltd * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of 'Shaven Puppy' nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.shavenpuppy.jglib.tools; import java.awt.*; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.font.*; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.image.*; import java.io.*; import java.util.ArrayList; import javax.swing.JFrame; import javax.swing.WindowConstants; import com.shavenpuppy.jglib.*; import com.shavenpuppy.jglib.Font; import com.shavenpuppy.jglib.Image; import com.shavenpuppy.jglib.util.Util; import tools.JGImageUtil; /** * Converts Java fonts into jgfonts. * Usage: * FontConverter <srcfont> <destfontfilename> <maxchars> * Srcfont is like Arial-BOLD-12. * Maxchars will be 127 for ascii or 65536 for unicode. */ public class FontConverter { private static final boolean DEBUG = false; /** Safety border between glyphs */ private static final int BORDER = 4; String srcFontName; String outputDir; java.awt.Font srcFont; Font destFont; int maxChars; boolean blur; /** * Lord have mercy. A graphics2d which is implemented by OpenGL. Whatever next? * This is just a hacked class to enable us to get at the glyphs being rendered by * a TextLayout object. */ private class GLGraphics2D extends Graphics2D { // Create temp dummy image which we can get a font rendering context from private final BufferedImage image; private final Graphics2D g2d; private final FontRenderContext frc; private final FontMetrics metrics; private float[] xxx = new float[2]; private float[] yyy = new float[2]; // Used when rendering a font private int numGlyphsDrawn; private int glyphPos; public GLGraphics2D() { image = new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR); g2d = (Graphics2D) image.getGraphics(); g2d.setFont(srcFont); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); frc = g2d.getFontRenderContext(); metrics = g2d.getFontMetrics(srcFont); } /* * These are the methods which are actually called when rendering a font with TextLayout */ @Override public java.awt.Font getFont() { return srcFont; } @Override public FontMetrics getFontMetrics(java.awt.Font f) { return metrics; } @Override public FontRenderContext getFontRenderContext() { return frc; } @Override public void drawGlyphVector(GlyphVector g, float x, float y) { final int n = g.getNumGlyphs(); for (int i = 0; i < n && i < 2; i++) { Point2D pos = g.getGlyphPosition(i); xxx[glyphPos] = (float) pos.getX() + x; yyy[glyphPos++] = (float) pos.getY() + y; numGlyphsDrawn++; } } public void reset() { glyphPos = 0; numGlyphsDrawn = 0; } public int getNumGlyphsDrawn() { return numGlyphsDrawn; } /* * The following methods are just stubs to create a concrete class. During font rendering with a TextLayout, * none of these methods are actually called, so we can get away with not implementing any of them properly. */ @Override public void addRenderingHints(java.util.Map hints) { } @Override public void clearRect(int x, int y, int width, int height) { } @Override public void clip(Shape s) { } @Override public void clipRect(int x, int y, int width, int height) { } @Override public void copyArea(int x, int y, int width, int height, int dx, int dy) { } @Override public Graphics create() { return this; } @Override public void dispose() { } @Override public void draw(Shape s) { } @Override public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) { } @Override public void drawImage(java.awt.image.BufferedImage img, java.awt.image.BufferedImageOp op, int x, int y) { } @Override public boolean drawImage( java.awt.Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, java.awt.Color bgcolor, java.awt.image.ImageObserver observer) { return false; } @Override public boolean drawImage( java.awt.Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, java.awt.image.ImageObserver observer) { return false; } @Override public boolean drawImage( java.awt.Image img, int x, int y, int width, int height, java.awt.Color bgcolor, java.awt.image.ImageObserver observer) { return false; } @Override public boolean drawImage(java.awt.Image img, int x, int y, int width, int height, java.awt.image.ImageObserver observer) { return false; } @Override public boolean drawImage(java.awt.Image img, int x, int y, java.awt.Color bgcolor, java.awt.image.ImageObserver observer) { return false; } @Override public boolean drawImage(java.awt.Image img, int x, int y, java.awt.image.ImageObserver observer) { return false; } @Override public boolean drawImage(java.awt.Image img, AffineTransform xform, java.awt.image.ImageObserver obs) { return false; } @Override public void drawLine(int x1, int y1, int x2, int y2) { } @Override public void drawOval(int x, int y, int width, int height) { } @Override public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) { } @Override public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) { } @Override public void drawRenderableImage(java.awt.image.renderable.RenderableImage img, AffineTransform xform) { } @Override public void drawRenderedImage(java.awt.image.RenderedImage img, AffineTransform xform) { } @Override public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { } @Override public void drawString(String s, float x, float y) { } @Override public void drawString(String str, int x, int y) { } @Override public void drawString(java.text.AttributedCharacterIterator iterator, float x, float y) { } @Override public void drawString(java.text.AttributedCharacterIterator iterator, int x, int y) { } @Override public void fill(Shape s) { } @Override public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) { } @Override public void fillOval(int x, int y, int width, int height) { } @Override public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) { } @Override public void fillRect(int x, int y, int width, int height) { } @Override public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { } @Override public java.awt.Color getBackground() { return null; } @Override public Shape getClip() { return null; } @Override public java.awt.Rectangle getClipBounds() { return null; } @Override public java.awt.Color getColor() { return null; } @Override public Composite getComposite() { return null; } @Override public GraphicsConfiguration getDeviceConfiguration() { return null; } @Override public Paint getPaint() { return null; } @Override public Object getRenderingHint(RenderingHints.Key hintKey) { return null; } @Override public RenderingHints getRenderingHints() { return null; } @Override public Stroke getStroke() { return null; } @Override public AffineTransform getTransform() { return null; } @Override public boolean hit(java.awt.Rectangle rect, Shape s, boolean onStroke) { return false; } @Override public void rotate(double theta) { } @Override public void rotate(double theta, double x, double y) { } @Override public void scale(double sx, double sy) { } @Override public void setBackground(java.awt.Color color) { } @Override public void setClip(int x, int y, int width, int height) { } @Override public void setClip(Shape clip) { } @Override public void setColor(java.awt.Color c) { } @Override public void setComposite(Composite comp) { } @Override public void setFont(java.awt.Font font) { } @Override public void setPaint(Paint paint) { } @Override public void setPaintMode() { } @Override public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue) { } @Override public void setRenderingHints(java.util.Map hints) { } @Override public void setStroke(Stroke s) { } @Override public void setTransform(AffineTransform tx) { } @Override public void setXORMode(java.awt.Color c1) { } @Override public void shear(double shx, double shy) { } @Override public void transform(AffineTransform tx) { } @Override public void translate(double tx, double ty) { } @Override public void translate(int x, int y) { } } /** * Constructor for FontConverter. * Creates a JGLIB font from the specified Java font. * The resulting font can be read with getFont(). */ public FontConverter(String srcFontName, int maxChars, boolean blur) { this.maxChars = maxChars; this.srcFontName = srcFontName; this.blur = blur; System.out.println("Exporting font '"+srcFontName+"' ("+maxChars+" chars)"); } /** * Font converter args: * <input font name> <output font file name> <maxchars> [blur] * Example: * FontConverter Arial-PLAIN-12 c:\Projects\Blah\arial-plain-12.glfont 65536 blur * @param args */ public static void main(String[] args) { try { for (int i = 0; i < args.length; i += 3) { File destFile = new File(args[i + 1]); File parentDir = destFile.getAbsoluteFile().getParentFile(); parentDir.mkdirs(); boolean blur = i < args.length - 3 && args[i + 3].equals("blur"); FontConverter fc = new FontConverter(args[i], Integer.parseInt(args[i + 2]), blur); if (blur) { i ++; } ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(destFile))); fc.doCreate(); oos.writeObject(fc.destFont); oos.flush(); oos.close(); System.out.println("Exported to file '"+destFile+"'"); } } catch (Exception e) { e.printStackTrace(); } } protected Font doCreate() { if (maxChars > Character.MAX_VALUE) { maxChars = Character.MAX_VALUE; } srcFont = java.awt.Font.decode(srcFontName); BufferedImage tempImage = new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR_PRE); Graphics2D gl2d = (Graphics2D) tempImage.getGraphics(); FontRenderContext frc = gl2d.getFontRenderContext(); // Create the glyphs for the first MAX_GLYPHS characters GlyphVector[] gv = new GlyphVector[maxChars]; int[] mapping = new int[maxChars]; boolean[] fixedWidth = new boolean[maxChars]; gl2d.setFont(srcFont); FontMetrics metrics = gl2d.getFontMetrics(); for (int i = 0; i < maxChars; i++) { int[] temp = new int[1]; temp[0] = i; GlyphVector tempgv = srcFont.createGlyphVector(frc, temp); gv[i] = tempgv; } // Work out character mappings for (char i = 0; i < maxChars; i ++) { char[] charToMap = new char[] {i}; GlyphVector tempgv = srcFont.createGlyphVector(frc, charToMap); mapping[i] = tempgv.getGlyphCode(0); if (i >= '0' && i <= '9') { fixedWidth[mapping[i]] = true; } } // Let's make a guess at what size we need by using 8*width of an M as the width // and fitting the characters in: int width = Util.nextPowerOf2((int) (8 * srcFont.getStringBounds("M", frc).getWidth())); int height = metrics.getHeight(); int x = 0, y = 0, maxy = 0; for (int i = 0; i < maxChars; i++) { if (gv[i] == null) { continue; } Shape shape = gv[i].getGlyphOutline(0); java.awt.Rectangle bounds = shape.getBounds(); if (bounds.width == 0 || bounds.height == 0) { continue; } // Start a new row if another character won't fit if (x + bounds.width + BORDER + (blur ? 2 : 0) > width) { x = 0; y += maxy / 2; System.out.println("Row height "+maxy+" new height now "+y); maxy = 0; } x += bounds.width + BORDER + (blur ? 2 : 0); maxy = Math.max(maxy, bounds.height + BORDER + (blur ? 2 : 0)); } // Now round the height to a legal OpenGL power-of-2 height = y+maxy/2; System.out.println("Adjusted to "+width+"x"+height+" max height "+(y+maxy)); while (height >= width && width <= 512) { height *= 0.5; width *= 2; } height = Util.nextPowerOf2(y+maxy); System.out.println("Adjusted to "+width+"x"+height); width = Math.min(1024, width); height = Math.min(1024, height); // Create a buffered image of this size and set up the font again: BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR_PRE); Graphics2D g2d = (Graphics2D) image.getGraphics(); g2d.setFont(srcFont); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); int numGlyphs = Math.min(maxChars, srcFont.getNumGlyphs()); Glyph[] glyph = new Glyph[numGlyphs]; for (int i = 0; i < numGlyphs; i ++) { glyph[i] = new Glyph(); } GLGraphics2D specialRenderer = new GLGraphics2D(); x = 0; y = 0; for (int i = 0; i < numGlyphs; i++) { if (gv[i] == null) { continue; } Shape shape = gv[i].getGlyphOutline(0); java.awt.Rectangle bounds = shape.getBounds(); // Because characters are drawn below and to the left of the "origin" (x,y) // we need to move them along a wee bit. The translation applied to the bounds // here moves the whole rectangle to a (0,0) origin int ox = bounds.x; int oy = bounds.y; // Start a new row if another character won't fit if (x + bounds.width + BORDER + (blur ? 2 : 0) >= image.getWidth()) { x = 0; y += maxy; maxy = 0; } // Draw the glyph so that it doesn't go over any other characters already drawn, // by moving it back to a 0,0 origin g2d.translate((blur ? 1 : 0) + x - ox, (blur ? 1 : 0) + y - oy); g2d.fill(shape); if (DEBUG) { // g2d.setColor(Color.RED); // g2d.drawRect(bounds.x, bounds.y, bounds.width - 1, bounds.height - 1); // g2d.drawRect(bounds.x + 1, bounds.y + 1, bounds.width - 3, bounds.height - 3); // g2d.drawRect(bounds.x + 2, bounds.y + 2, bounds.width - 5, bounds.height - 5); // g2d.drawRect(bounds.x + 3, bounds.y + 3, bounds.width - 7, bounds.height - 7); // // for (int xxx = 0; xxx < bounds.width - 5; xxx += 4) { // boolean draw = (xxx & 4) == 4; // for (int yyy = 0; yyy < bounds.height - 5; yyy += 4) { // draw = !draw; // if (draw) { // g2d.fillRect(xxx + bounds.x, yyy + bounds.y, 4, 4); // } // } // } // // g2d.setColor(Color.WHITE); } g2d.translate((blur ? -1 : 0) - (x - ox), (blur ? -1 : 0) - (y - oy)); GlyphMetrics gmetrics = gv[i].getGlyphMetrics(0); float glyphAdvance; if (fixedWidth[i]) { glyphAdvance = gv[mapping['0']].getGlyphMetrics(0).getAdvance(); ox = (int)((glyphAdvance - bounds.width) / 2.0f); // System.out.println(ox); } else { glyphAdvance = gmetrics.getAdvance(); } // Calculate kerning with all other glyphs. ArrayList kerningList = new ArrayList(); ArrayList kernsWithList = new ArrayList(); for (int left = 0; left < numGlyphs; left ++) { if (gv[left] == null) { continue; } GlyphVector kerningVector = srcFont.createGlyphVector(frc, new int[] {left, i}); specialRenderer.reset(); specialRenderer.drawGlyphVector(kerningVector, 0.0f, 0.0f); GlyphMetrics gmetrics2 = gv[left].getGlyphMetrics(0); float glyphAdvance2 = gmetrics2.getAdvance(); float xdif = (specialRenderer.xxx[1] - specialRenderer.xxx[0]); if (xdif != glyphAdvance2) { kernsWithList.add(glyph[left]); kerningList.add(new Integer((int)Math.rint(0.25 + xdif - glyphAdvance2))); //System.out.println(i + " kerns with "+left+" : "+ ((int) xdif - glyphAdvance)); } } Glyph[] kernsWith; int[] kerning; if (kerningList.size() > 0) { kernsWith = new Glyph[kernsWithList.size()]; kernsWithList.toArray(kernsWith); kerning = new int[kerningList.size()]; for (int q = 0; q < kerningList.size(); q ++) { kerning[q] = ((Integer) kerningList.get(q)).intValue(); } } else { kernsWith = null; kerning = null; } glyph[i].init ( x - (blur ? 1 : 0), y - (blur ? 1 : 0), bounds.width + (blur ? 2 : 0), bounds.height + (blur ? 2 : 0), ox - (blur ? 1 : 0), (-(bounds.height + oy)) - (blur ? 1 : 0), (int) Math.floor(glyphAdvance), kernsWith, kerning ); x += bounds.width + BORDER + (blur ? 2 : 0) ; // +2 just in case maxy = Math.max(maxy, bounds.height + BORDER + (blur ? 2 : 0)); } // Blur the image BufferedImage blurredImage; if (blur) { float[] matrix = { 0.025f, 0.050f, 0.025f, 0.050f, 0.700f, 0.050f, 0.025f, 0.050f, 0.025f, }; BufferedImageOp op = new ConvolveOp(new Kernel(3, 3, matrix)); blurredImage = op.filter(image, null); } else { blurredImage = image; } // Now put buffered image back into font image int h = blurredImage.getHeight(); while (y + maxy < h / 2) { h /= 2; } blurredImage = blurredImage.getSubimage(0, 0, blurredImage.getWidth(), h); byte[] newRenderedImage = (byte[]) blurredImage.getRaster().getDataElements(0, 0, blurredImage.getWidth(), blurredImage.getHeight(), null); destFont = new Font( srcFont.getName(), srcFont.isBold(), srcFont.isItalic(), new Image(blurredImage.getWidth(), blurredImage.getHeight(), Image.LUMINANCE_ALPHA), glyph, srcFont.getSize(), metrics.getMaxAscent(), metrics.getMaxDescent(), metrics.getLeading(), mapping); for (y = 0; y < h; y++) { for (x = 0; x < blurredImage.getWidth(); x++) { int pos = y * blurredImage.getWidth() * BORDER + x * BORDER; byte alpha = newRenderedImage[pos + 3]; byte img = newRenderedImage[pos + 1]; destFont.getImage().getData().put(img); destFont.getImage().getData().put(alpha); } } destFont.getImage().getData().rewind(); if (DEBUG) { final BufferedImage img = blurredImage; new JFrame() { private static final long serialVersionUID = 1L; { addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { dispose(); } }); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); setSize(img.getWidth() * 2, img.getHeight() * 2+ 31); setVisible(true); } /* (non-Javadoc) * @see java.awt.Container#paint(java.awt.Graphics) */ @Override public void paint(Graphics g) { Graphics2D g2dDebug = (Graphics2D) g; g2dDebug.setColor(java.awt.Color.black); g2dDebug.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2dDebug.fillRect(0, 32, img.getWidth() * 2, img.getHeight() * 2); g2dDebug.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER)); g2dDebug.drawImage(img, 0, 31, img.getWidth() * 2, img.getHeight() * 2, this); } }; } return destFont; } /** * @return the created font */ public Font getFont() { return destFont; } }